﻿/* Copyright (c) 2010 EPiServer AB
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Collections;

namespace EPiServer.CdnSupport
{
    /// <summary>
    /// Class used internally to store cache objects
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class MicroCacheItem<T>
    {
        public T Value;
        public DateTime Added;
        public long Usage;
    }

    /// <summary>
    /// A tiny cache that is optimized to be really fast while offloading other lookups (rather than caching everything perfectly)
    /// </summary>
    public class MicroCache<T> : IDisposable
    {
        private Dictionary<string, MicroCacheItem<T>>   _cache;
        private ReaderWriterLockSlim                    _lock = new ReaderWriterLockSlim();
        private Timer                                   _timer;
        private TimeSpan                                _cacheTimeout;
        private int                                     _maxCount;
        private double                                  _cleanUpThreshold;

        private long _cacheHits;
        private long _cacheMisses;
        private long _cacheBusy;
        private long _cacheAdded;
        private long _cacheRemoved;

        public static readonly TimeSpan DefaultCacheTimeout     = TimeSpan.FromMinutes(10);
        public static readonly TimeSpan DefaultCleanupInterval  = TimeSpan.FromMinutes(2);
        public const int                DefaultMaxCount         = 10000;
        public const double             DefaultCleanupThreshold = 0.75;


        public MicroCache(bool enableAutoCleaner)
            : this(enableAutoCleaner, DefaultCacheTimeout, DefaultCleanupInterval, DefaultMaxCount, DefaultCleanupThreshold)
        {

        }
        public MicroCache(bool enableAutoCleaner, TimeSpan cacheTimeout, TimeSpan cleanupInterval, int maxCount, double cleanUpThreshold)
        {
            _cleanUpThreshold = cleanUpThreshold;
            _maxCount = maxCount;
            _cacheTimeout = cacheTimeout;
            _cache = new Dictionary<string, MicroCacheItem<T>>(_maxCount, StringComparer.InvariantCultureIgnoreCase);
            if(enableAutoCleaner)
                _timer = new Timer(new TimerCallback(CleanupCallback), null, cleanupInterval, cleanupInterval);
        }

        /// <summary>
        /// The number of items in cache
        /// </summary>
        /// <remarks>Be careful with this property since it applies a read lock</remarks>
        public int Count
        {
            get
            {
                try
                {
                    _lock.EnterReadLock();
                    return _cache.Count;
                }
                finally
                {
                    _lock.ExitReadLock();
                }
            }
        }

        /// <summary>
        /// The number of returned objects from the cache
        /// </summary>
        public long CacheHits
        {
            get { return _cacheHits; }
        }

        /// <summary>
        /// The number of requests for objects that werent in the cache
        /// </summary>
        public long CacheMisses
        {
            get { return _cacheMisses; }
        }

        /// <summary>
        /// The number of times an operation did not complete because the cache was busy (locked)
        /// </summary>
        public long CacheBusy
        {
            get { return _cacheBusy; }
        }

        /// <summary>
        /// The number of times something has been added to the cache
        /// </summary>
        public long CacheAdded
        {
            get { return _cacheAdded; }
        }

        /// <summary>
        /// The number of times something has been removed from the cache
        /// </summary>
        public long CacheRemoved
        {
            get { return _cacheRemoved; }
        }

        /// <summary>
        /// Get a value from the cache
        /// </summary>
        /// <param name="key"></param>
        public T Get(string key, out bool cacheBusy)
        {
            // Since this method is performance critical we never wait to get
            // read locks, just return if someone is writing to the cache
            if (!_lock.TryEnterReadLock(0))
            {
                Interlocked.Increment(ref _cacheBusy);
                cacheBusy = true;
                return default(T);
            }

            try
            {

                cacheBusy = false;

                MicroCacheItem<T> foundValue;
                if (_cache.TryGetValue(key, out foundValue))
                {
                    Interlocked.Increment(ref _cacheHits);
                    Interlocked.Increment(ref foundValue.Usage);
                    return foundValue.Value;
                }
                else
                {
                    Interlocked.Increment(ref _cacheMisses);
                    
                    // Make sure the caller don't try to add stuff to the cache
                    // when its full because this will kill the performance
                    if (_cache.Count >= _maxCount)
                    {
                        cacheBusy = true;
                    }
                    return default(T);
                }
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// Add stuff to the cache
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <remarks>There is no guarantee that the item willl actually be added to the cache</remarks>
        public bool Add(string key, T value)
        {
            // We need a small wait time here to ensure that we actually get
            // add operations done when on high load but we don't want to block
            // anything so we do a quick exit if can't get a write lock
            if (!_lock.TryEnterWriteLock(1))
            {
                Interlocked.Increment(ref _cacheBusy);
                return false;
            }

            try
            {
                // The cache is full, so don't add anything, this should have been signaled in
                // the Get-method so this should not be a very common scenario
                if (_cache.Count >= _maxCount)
                {
                    Interlocked.Increment(ref _cacheBusy);
                    return false;
                }

                Interlocked.Increment(ref _cacheAdded);
                _cache[key] = new MicroCacheItem<T>() { Value = value, Added = DateTime.UtcNow };
                return true;
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        /// Remove stuff from the cache
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <remarks>There is no guarantee that the item willl actually be removed from the cache</remarks>
        public bool Remove(string key)
        {
            // The upgradable lock is to make sure we never take write locks when stuff are not found
            _lock.EnterUpgradeableReadLock();
            try
            {
                Interlocked.Increment(ref _cacheRemoved);
                if (_cache.ContainsKey(key))
                {
                    _lock.EnterWriteLock();
                    try
                    {
                        _cache.Remove(key);
                        return true;
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                }
                else
                {
                    return false;
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Clean up "old" stuff from the cache
        /// </summary>
        public void CleanUp()
        {
            List<string> backlog = new List<string>();
            DateTime killZone = DateTime.UtcNow - _cacheTimeout;
            
            _lock.EnterUpgradeableReadLock();

            try
            {
                foreach(KeyValuePair<string,MicroCacheItem<T>> val in _cache)
                {
                    if (val.Value.Added < killZone)
                    {
                        backlog.Add(val.Key);
                    }
                }

                if (backlog.Count > 0)
                {
                    _lock.EnterWriteLock();
                    try
                    {
                        foreach (string key in backlog)
                        {
                            _cache.Remove(key);
                        }
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                }

                if (_cache.Count > _cleanUpThreshold * _maxCount)
                {

                    int takeCount = Convert.ToInt32((1 - _cleanUpThreshold) * _maxCount);
                    var x = (from t in _cache
                             orderby t.Value.Usage descending
                             select t.Key).Take(takeCount);

                    _lock.EnterWriteLock();
                    try
                    {

                        foreach (string key in x)
                        {
                            _cache.Remove(key);
                        }
                    }
                    finally
                    {
                        _lock.ExitWriteLock();
                    }
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Callback from Timer
        /// </summary>
        /// <param name="state"></param>
        private void CleanupCallback(object state)
        {
            CleanUp();
        }

        public void Dispose()
        {
            if (_timer != null)
            {
                _timer.Dispose();
                _timer = null;
            }
        }
    }
}
